#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>
#include <EGL/egl.h>
#include <X11/Xlib.h>

#ifdef USE_XRANDR
#include <X11/extensions/Xrandr.h>
#endif

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <float.h>
#include <time.h>
#include <string>
#include <iostream>
#include <sstream>
#include <iomanip>
#include <errno.h>

#include "amd_perf_monitor.hpp"
#include "testbed.hpp"

namespace testbed {

const char arg_prefix[]					= "-";
const char arg_app[]					= "app";

}

static const char arg_nframes[]			= "frames";
static const char arg_screen[]			= "screen";
static const char arg_bitness[]			= "bitness";
static const char arg_config_id[]		= "config_id";
static const char arg_fsaa[]			= "fsaa";
static const char arg_grab[]			= "grab_frame";
static const char arg_print_configs[]	= "print_egl_configs";
static const char arg_print_perf[]		= "print_perf_counters";

static Atom wm_protocols;
static Atom wm_delete_window;


static uint64_t
timer_nsec()
{
	timespec t;
	clock_gettime(CLOCK_MONOTONIC, &t);

	return t.tv_sec * 1000000000ULL + t.tv_nsec;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// EGL helper
////////////////////////////////////////////////////////////////////////////////////////////////////

struct EGL
{
	EGLDisplay display;
	EGLContext context;
	EGLSurface surface;

	EGL()
	: display(EGL_NO_DISPLAY)
	, context(EGL_NO_CONTEXT)
	, surface(EGL_NO_SURFACE)
	{}

	bool initGLES2(
		Display* xdisplay,
		const Window& window,
		const unsigned config_id,
		const unsigned fsaa,
		const unsigned nbits_r,
		const unsigned nbits_g,
		const unsigned nbits_b,
		const unsigned nbits_a,
		const bool print_configs);

	void deinit();
	void swapBuffers() const;
};


static std::string
string_from_EGL_error(
	const EGLint error)
{
	switch (error) {

	case EGL_SUCCESS:
		return "EGL_SUCCESS";

	case EGL_NOT_INITIALIZED:
		return "EGL_NOT_INITIALIZED";

	case EGL_BAD_ACCESS:
		return "EGL_BAD_ACCESS";

	case EGL_BAD_ALLOC:
		return "EGL_BAD_ALLOC";

	case EGL_BAD_ATTRIBUTE:
		return "EGL_BAD_ATTRIBUTE";

	case EGL_BAD_CONTEXT:
		return "EGL_BAD_CONTEXT";

	case EGL_BAD_CONFIG:
		return "EGL_BAD_CONFIG";

	case EGL_BAD_CURRENT_SURFACE:
		return "EGL_BAD_CURRENT_SURFACE";

	case EGL_BAD_DISPLAY:
		return "EGL_BAD_DISPLAY";

	case EGL_BAD_SURFACE:
		return "EGL_BAD_SURFACE";

	case EGL_BAD_MATCH:
		return "EGL_BAD_MATCH";

	case EGL_BAD_PARAMETER:
		return "EGL_BAD_PARAMETER";

	case EGL_BAD_NATIVE_PIXMAP:
		return "EGL_BAD_NATIVE_PIXMAP";

	case EGL_BAD_NATIVE_WINDOW:
		return "EGL_BAD_NATIVE_WINDOW";

	case EGL_CONTEXT_LOST:
		return "EGL_CONTEXT_LOST";
	}

	std::ostringstream s;
	s << "unknown EGL error (0x" << std::hex << std::setw(8) << std::setfill('0') << error << std::dec << ")";

	return s.str();
}


bool
testbed::util::reportEGLError(
	std::ostream& stream)
{
	const EGLint error = eglGetError();

	if (EGL_SUCCESS == error)
		return false;

	stream << "EGL error: " << 
		string_from_EGL_error(error) << std::endl;

	return true;
}

static std::string
string_from_EGL_attrib(
	const EGLint attr)
{
	switch (attr) {

	case EGL_BUFFER_SIZE:
		return "EGL_BUFFER_SIZE";

	case EGL_ALPHA_SIZE:
		return "EGL_ALPHA_SIZE";

	case EGL_BLUE_SIZE:
		return "EGL_BLUE_SIZE";

	case EGL_GREEN_SIZE:
		return "EGL_GREEN_SIZE";

	case EGL_RED_SIZE:
		return "EGL_RED_SIZE";

	case EGL_DEPTH_SIZE:
		return "EGL_DEPTH_SIZE";

	case EGL_STENCIL_SIZE:
		return "EGL_STENCIL_SIZE";

	case EGL_CONFIG_CAVEAT:
		return "EGL_CONFIG_CAVEAT";

	case EGL_CONFIG_ID:
		return "EGL_CONFIG_ID";

	case EGL_LEVEL:
		return "EGL_LEVEL";

	case EGL_MAX_PBUFFER_HEIGHT:
		return "EGL_MAX_PBUFFER_HEIGHT";

	case EGL_MAX_PBUFFER_PIXELS:
		return "EGL_MAX_PBUFFER_PIXELS";

	case EGL_MAX_PBUFFER_WIDTH:
		return "EGL_MAX_PBUFFER_WIDTH";

	case EGL_NATIVE_RENDERABLE:
		return "EGL_NATIVE_RENDERABLE";

	case EGL_NATIVE_VISUAL_ID:
		return "EGL_NATIVE_VISUAL_ID";

	case EGL_NATIVE_VISUAL_TYPE:
		return "EGL_NATIVE_VISUAL_TYPE";

	case EGL_SAMPLES:
		return "EGL_SAMPLES";

	case EGL_SAMPLE_BUFFERS:
		return "EGL_SAMPLE_BUFFERS";

	case EGL_SURFACE_TYPE:
		return "EGL_SURFACE_TYPE";

	case EGL_TRANSPARENT_TYPE:
		return "EGL_TRANSPARENT_TYPE";

	case EGL_TRANSPARENT_BLUE_VALUE:
		return "EGL_TRANSPARENT_BLUE_VALUE";

	case EGL_TRANSPARENT_GREEN_VALUE:
		return "EGL_TRANSPARENT_GREEN_VALUE";

	case EGL_TRANSPARENT_RED_VALUE:
		return "EGL_TRANSPARENT_RED_VALUE";

	case EGL_BIND_TO_TEXTURE_RGB:
		return "EGL_BIND_TO_TEXTURE_RGB";

	case EGL_BIND_TO_TEXTURE_RGBA:
		return "EGL_BIND_TO_TEXTURE_RGBA";

	case EGL_MIN_SWAP_INTERVAL:
		return "EGL_MIN_SWAP_INTERVAL";

	case EGL_MAX_SWAP_INTERVAL:
		return "EGL_MAX_SWAP_INTERVAL";

	case EGL_LUMINANCE_SIZE:
		return "EGL_LUMINANCE_SIZE";

	case EGL_ALPHA_MASK_SIZE:
		return "EGL_ALPHA_MASK_SIZE";

	case EGL_COLOR_BUFFER_TYPE:
		return "EGL_COLOR_BUFFER_TYPE";

	case EGL_RENDERABLE_TYPE:
		return "EGL_RENDERABLE_TYPE";

	case EGL_CONFORMANT:
		return "EGL_CONFORMANT";
	}

	std::ostringstream s;
	s << "unknown EGL attribute (0x" << std::hex << std::setw(8) << std::setfill('0') << attr << std::dec << ")";

	return s.str();
}


bool EGL::initGLES2(
	Display* xdisplay,
	const Window& window,
	const unsigned config_id,
	const unsigned fsaa,
	const unsigned nbits_r,
	const unsigned nbits_g,
	const unsigned nbits_b,
	const unsigned nbits_a,
	const bool print_configs)
{
	const unsigned nbits_pixel =
		nbits_r +
		nbits_g +
		nbits_b +
		nbits_a + 7 & ~7;

	if (!config_id && !nbits_pixel)
	{
		std::cerr << "nil pixel size requested; abort" << std::endl;

		return false;
	}

	deinit();

	display = eglGetDisplay(xdisplay /*EGL_DEFAULT_DISPLAY*/);

 	EGLint major;
 	EGLint minor;

	if (EGL_FALSE == eglInitialize(display, &major, &minor))
	{
		std::cerr << "eglInitialize() failed" << std::endl;
		testbed::util::reportEGLError();

		// don't deinit, just reset display, as no initialisation has occurred
		display = EGL_NO_DISPLAY;

		return false;
	}

	std::cout << "eglInitialize() succeeded; major: " << major << ", minor: " << minor << std::endl;

	const char* str_version	= eglQueryString(display, EGL_VERSION);
	const char* str_vendor	= eglQueryString(display, EGL_VENDOR);
	const char* str_exten	= eglQueryString(display, EGL_EXTENSIONS);

	std::cout << "egl version, vendor, extensions:"
		"\n\t" << str_version <<
		"\n\t" << str_vendor <<
		"\n\t" << str_exten << std::endl;

	eglBindAPI(EGL_OPENGL_ES_API);

	EGLConfig config[128];
	EGLint num_config;

	if (EGL_FALSE == eglGetConfigs(display, config, EGLint(sizeof(config) / sizeof(config[0])), &num_config))
	{
		std::cerr << "eglGetConfigs() failed" << std::endl;
		testbed::util::reportEGLError();

		deinit();

		return false;
	}

	for (EGLint i = 0; i < num_config && print_configs; ++i)
	{
		std::cout << "\nconfig " << i << ":\n";

		static const EGLint attr[] =
		{
			EGL_BUFFER_SIZE,
			EGL_ALPHA_SIZE,
			EGL_BLUE_SIZE,
			EGL_GREEN_SIZE,
			EGL_RED_SIZE,
			EGL_DEPTH_SIZE,
			EGL_STENCIL_SIZE,
			EGL_CONFIG_CAVEAT,
			EGL_CONFIG_ID,
			EGL_LEVEL,
			EGL_MAX_PBUFFER_HEIGHT,
			EGL_MAX_PBUFFER_PIXELS,
			EGL_MAX_PBUFFER_WIDTH,
			EGL_NATIVE_RENDERABLE,
			EGL_NATIVE_VISUAL_ID,
			EGL_NATIVE_VISUAL_TYPE,
			EGL_SAMPLES,
			EGL_SAMPLE_BUFFERS,
			EGL_SURFACE_TYPE,
			EGL_TRANSPARENT_TYPE,
			EGL_TRANSPARENT_BLUE_VALUE,
			EGL_TRANSPARENT_GREEN_VALUE,
			EGL_TRANSPARENT_RED_VALUE,
			EGL_BIND_TO_TEXTURE_RGB,
			EGL_BIND_TO_TEXTURE_RGBA,
			EGL_MIN_SWAP_INTERVAL,
			EGL_MAX_SWAP_INTERVAL,
			EGL_LUMINANCE_SIZE,
			EGL_ALPHA_MASK_SIZE,
			EGL_COLOR_BUFFER_TYPE,
			EGL_RENDERABLE_TYPE,
			EGL_CONFORMANT
		};

		for (unsigned j = 0; j < sizeof(attr) / sizeof(attr[0]); ++j)
		{
			EGLint value;

			if (EGL_FALSE == eglGetConfigAttrib(display, config[i], attr[j], &value))
			{
				std::cerr << "eglGetConfigAttrib() failed" << std::endl;
				continue;
			}

			std::cout << string_from_EGL_attrib(attr[j]) << " 0x" <<
				std::hex << std::setw(8) << std::setfill('0') << value << std::dec << std::endl;
		}
	}

	EGLint attr[64];
	unsigned na = 0;

	if (!config_id)
	{
		attr[na++] = EGL_BUFFER_SIZE;
		attr[na++] = EGLint(nbits_pixel);
		attr[na++] = EGL_RED_SIZE;
		attr[na++] = EGLint(nbits_r);
		attr[na++] = EGL_GREEN_SIZE;
		attr[na++] = EGLint(nbits_g);
		attr[na++] = EGL_BLUE_SIZE;
		attr[na++] = EGLint(nbits_b);
		attr[na++] = EGL_ALPHA_SIZE;
		attr[na++] = EGLint(nbits_a);
		attr[na++] = EGL_SURFACE_TYPE;
		attr[na++] = EGL_WINDOW_BIT;
		attr[na++] = EGL_RENDERABLE_TYPE;
		attr[na++] = EGL_OPENGL_ES2_BIT;

		if (testbed::hook::requires_depth())
		{
			attr[na++] = EGL_DEPTH_SIZE;
			attr[na++] = 16;
		}

		if (fsaa)
		{
			attr[na++] = EGL_SAMPLE_BUFFERS;
			attr[na++] = 1;
			attr[na++] = EGL_SAMPLES;
			attr[na++] = EGLint(fsaa);
		}
		else
		{
			attr[na++] = EGL_CONFIG_CAVEAT;
			attr[na++] = EGL_NONE;
		}
	}
	else
	{
		attr[na++] = EGL_CONFIG_ID;
		attr[na++] = EGLint(config_id);
	}

	assert(na < sizeof(attr) / sizeof(attr[0]));

	attr[na] = EGL_NONE;

	if (EGL_FALSE == eglChooseConfig(display, attr, config,
			EGLint(sizeof(config) / sizeof(config[0])), &num_config))
	{
		std::cerr << "eglChooseConfig() failed" << std::endl;
		testbed::util::reportEGLError();

		deinit();

		return false;
	}

	unsigned best_match = 0;

	for (EGLint i = 1; i < num_config; ++i)
	{
		EGLint value[2];

		eglGetConfigAttrib(display, config[best_match], EGL_CONFIG_CAVEAT, &value[0]);
		eglGetConfigAttrib(display, config[i], EGL_CONFIG_CAVEAT, &value[1]);

		if (value[1] == EGL_SLOW_CONFIG)
			continue;

		if (value[0] == EGL_SLOW_CONFIG)
		{
			best_match = i;
			continue;
		}

		eglGetConfigAttrib(display, config[best_match], EGL_BUFFER_SIZE, &value[0]);
		eglGetConfigAttrib(display, config[i], EGL_BUFFER_SIZE, &value[1]);

		if (value[0] < value[1])
			continue;

		eglGetConfigAttrib(display, config[best_match], EGL_ALPHA_SIZE, &value[0]);
		eglGetConfigAttrib(display, config[i], EGL_ALPHA_SIZE, &value[1]);

		if (value[0] < value[1])
			continue;

		eglGetConfigAttrib(display, config[best_match], EGL_DEPTH_SIZE, &value[0]);
		eglGetConfigAttrib(display, config[i], EGL_DEPTH_SIZE, &value[1]);

		if (value[0] < value[1])
			continue;

		eglGetConfigAttrib(display, config[best_match], EGL_SAMPLES, &value[0]);
		eglGetConfigAttrib(display, config[i], EGL_SAMPLES, &value[1]);

		if (value[0] < value[1])
			continue;

		best_match = i;
	}

	EGLint value;
	eglGetConfigAttrib(display, config[best_match], EGL_CONFIG_ID, &value);

	std::cout << "\nchoosing configs returned " << num_config << " candidate(s), best match is EGL_CONFIG_ID 0x" <<
		std::hex << std::setw(8) << std::setfill('0') << value << std::dec << "\n" << std::endl;

	const EGLint context_attr[] =
	{
		EGL_CONTEXT_CLIENT_VERSION, 2,
		EGL_NONE
	};

	context = eglCreateContext(display, config[best_match], EGL_NO_CONTEXT, context_attr);

	if (EGL_NO_CONTEXT == context)
	{
		std::cerr << "eglCreateContext() failed" << std::endl;
		testbed::util::reportEGLError();

		deinit();

		return false;
	}

	surface = eglCreateWindowSurface(display, config[best_match], window, 0);

	if (EGL_NO_SURFACE == surface)
	{
		std::cerr << "eglCreateWindowSurface() failed" << std::endl;
		testbed::util::reportEGLError();

		deinit();

		return false;
	}

	if (EGL_FALSE == eglSurfaceAttrib(display, surface, EGL_SWAP_BEHAVIOR, EGL_BUFFER_DESTROYED))
	{
		std::cerr << "eglSurfaceAttrib() failed" << std::endl;
		testbed::util::reportEGLError();

		deinit();

		return false;
	}

	if (EGL_FALSE == eglMakeCurrent(display, surface, surface, context))
	{
		std::cerr << "eglMakeCurrent() failed" << std::endl;
		testbed::util::reportEGLError();

		deinit();

		return false;
	}

	return true;
}


void EGL::deinit()
{
	if (EGL_NO_SURFACE != surface)
	{
		eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
		eglDestroySurface(display, surface);

		surface = EGL_NO_SURFACE;
	}

	if (EGL_NO_CONTEXT != context)
	{
		eglDestroyContext(display, context);

		context = EGL_NO_CONTEXT;
	}

	if (EGL_NO_DISPLAY != display)
	{
		eglTerminate(display);

		display = EGL_NO_DISPLAY;
	}
}


void EGL::swapBuffers() const
{
	eglSwapBuffers(display, surface);
}


static std::string
string_from_GL_error(
	const GLenum error)
{
	switch (error) {

	case GL_NO_ERROR:
		return "GL_NO_ERROR";

	case GL_INVALID_ENUM:
		return "GL_INVALID_ENUM";

	case GL_INVALID_FRAMEBUFFER_OPERATION:
		return "GL_INVALID_FRAMEBUFFER_OPERATION";

	case GL_INVALID_VALUE:
		return "GL_INVALID_VALUE";

	case GL_INVALID_OPERATION:
		return "GL_INVALID_OPERATION";

	case GL_OUT_OF_MEMORY:
		return "GL_OUT_OF_MEMORY";
	}

	std::ostringstream s;
	s << "unknown GL error (0x" << std::hex << std::setw(8) << std::setfill('0') << error << std::dec << ")";

	return s.str();
}


bool
testbed::util::reportGLError(
	std::ostream& stream)
{
	const GLenum error = glGetError();

	if (GL_NO_ERROR == error)
		return false;

	stream << "GL error: " << string_from_GL_error(error) << std::endl;

	return true;
}


static bool
reportGLCaps()
{
	const GLubyte* str_version	= glGetString(GL_VERSION);
	const GLubyte* str_vendor	= glGetString(GL_VENDOR);
	const GLubyte* str_renderer	= glGetString(GL_RENDERER);
	const GLubyte* str_glsl_ver	= glGetString(GL_SHADING_LANGUAGE_VERSION);
	const GLubyte* str_exten	= glGetString(GL_EXTENSIONS);

	std::cout << "gl version, vendor, renderer, glsl version, extensions:"
		"\n\t" << (const char*) str_version <<
		"\n\t" << (const char*) str_vendor << 
		"\n\t" << (const char*) str_renderer <<
		"\n\t" << (const char*) str_glsl_ver <<
		"\n\t" << (const char*) str_exten << "\n" << std::endl;

	GLint params[2]; // we won't need more than 2

	glGetIntegerv(GL_MAX_TEXTURE_SIZE, params);
	std::cout << "GL_MAX_TEXTURE_SIZE: " << params[0] << std::endl;

	glGetIntegerv(GL_MAX_CUBE_MAP_TEXTURE_SIZE, params);
	std::cout << "GL_MAX_CUBE_MAP_TEXTURE_SIZE: " << params[0] << std::endl;

	glGetIntegerv(GL_MAX_VIEWPORT_DIMS, params);
	std::cout << "GL_MAX_VIEWPORT_DIMS: " << params[0] << ", " << params[1] << std::endl;

	glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, params);
	std::cout << "GL_MAX_RENDERBUFFER_SIZE: " << params[0] << std::endl;

	glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, params);
	std::cout << "GL_MAX_VERTEX_ATTRIBS: " << params[0] << std::endl;

	glGetIntegerv(GL_MAX_VERTEX_UNIFORM_VECTORS, params);
	std::cout << "GL_MAX_VERTEX_UNIFORM_VECTORS: " << params[0] << std::endl;

	glGetIntegerv(GL_MAX_VARYING_VECTORS, params);
	std::cout << "GL_MAX_VARYING_VECTORS: " << params[0] << std::endl;

	glGetIntegerv(GL_MAX_FRAGMENT_UNIFORM_VECTORS, params);
	std::cout << "GL_MAX_FRAGMENT_UNIFORM_VECTORS: " << params[0] << std::endl;

	glGetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, params);
	std::cout << "GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS: " << params[0] << std::endl;

	glGetIntegerv(GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS, params);
	std::cout << "GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS: " << params[0] << std::endl;

	glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, params);
	std::cout << "GL_MAX_TEXTURE_IMAGE_UNITS: " << params[0] << std::endl;

	std::cout << std::endl;

	return true;
}


void
testbed::util::fill_with_checker(
	pix* const buffer,
	const unsigned stride,
	const unsigned dim_x,
	const unsigned dim_y,
	const unsigned granularity)
{
	const pix pixA(255U, 128U, 128U);
	const pix pixB(128U, 128U, 255U);

	for (unsigned y = 0; y < dim_y; ++y)
		for (unsigned x = 0; x < dim_x; ++x)
		{
			*((pix*) ((uint8_t*) buffer + y * stride) + x) = (y & granularity) ^ (x & granularity)
				? pixB
				: pixA;
		}
}


bool
testbed::util::fill_from_file(
	pix* const buffer,
	const unsigned stride,
	const unsigned dim_x,
	const unsigned dim_y,
	const char* filename)
{
	FILE* file = fopen(filename, "rb");

	if (!file)
	{
		std::cerr << __FUNCTION__ << " failed to open file '" << filename << "'" << std::endl;

		return false;
	}

	if (0 != fseek(file, 0, SEEK_END))
	{
		std::cerr << __FUNCTION__ << " failed to seek through the end of file '" << filename << "'" << std::endl;

		return false;
	}

	const unsigned pix_size = sizeof(pix);
	const unsigned row_size = dim_x * pix_size;
	const unsigned img_size = dim_y * row_size;

	const long len = ftell(file);

	// skip optional dimensions record (8 bytes)
	if (len == img_size + 8)
		fseek(file, 8, SEEK_SET);
	else
		fseek(file, 0, SEEK_SET);

	unsigned read = 0;

	if (row_size != stride)
	{
		pix* const tmp = (pix*) malloc(img_size);

		read = fread(tmp, 1, img_size, file);

		for (unsigned i = 0; i < dim_y; ++i)
			memcpy((uint8_t*) buffer + i * stride, tmp + i * dim_x, row_size);

		free(tmp);
	}
	else
		read = fread(buffer, 1, img_size, file);

	fclose(file);

	if (img_size != read)
	{
		std::cerr << __FUNCTION__ << " read " << read << " bytes from file '" << filename << "' "
			"(expected " << img_size << ")" << std::endl;

		return false;
	}

	return true;
}


void
testbed::util::fill_YUV420_from_RGB(
	uint8_t* const y_buffer,
	uint8_t* const u_buffer,
	uint8_t* const v_buffer,
	const unsigned y_stride,
	const unsigned u_stride,
	const unsigned v_stride,
	pix* const rgb_buffer,
	const unsigned rgb_stride,
	const unsigned dim_x,
	const unsigned dim_y)
{
	const pix* rgb_src[2] =
	{
		rgb_buffer,
		(pix*) ((uint8_t*) rgb_buffer + rgb_stride)
	};

	uint8_t* y_dst[2] =
	{
		y_buffer,
		y_buffer + y_stride
	};

	uint8_t* u_dst = u_buffer;
	uint8_t* v_dst = v_buffer;

	const unsigned rgb_pad = rgb_stride - dim_x * sizeof(pix);
	const unsigned y_pad = y_stride - dim_x;
	const unsigned u_pad = u_stride - dim_x / 2;
	const unsigned v_pad = v_stride - dim_x / 2;

	for (unsigned i = 0; i < dim_y / 2; ++i)
	{
		for (unsigned j = 0; j < dim_x / 2; ++j)
		{
			const float rgb[4][3] =
			{
				{
					rgb_src[0][0].c[0] / 255.f,
					rgb_src[0][0].c[1] / 255.f,
					rgb_src[0][0].c[2] / 255.f
				},
				{
					rgb_src[0][1].c[0] / 255.f,
					rgb_src[0][1].c[1] / 255.f,
					rgb_src[0][1].c[2] / 255.f
				},
				{
					rgb_src[1][0].c[0] / 255.f,
					rgb_src[1][0].c[1] / 255.f,
					rgb_src[1][0].c[2] / 255.f
				},
				{
					rgb_src[1][1].c[0] / 255.f,
					rgb_src[1][1].c[1] / 255.f,
					rgb_src[1][1].c[2] / 255.f
				}
			};

			const float rgb_filt[3] =
			{
				(rgb[0][0] + rgb[1][0] + rgb[2][0] + rgb[3][0]) * .25f,
				(rgb[0][1] + rgb[1][1] + rgb[2][1] + rgb[3][1]) * .25f,
				(rgb[0][2] + rgb[1][2] + rgb[2][2] + rgb[3][2]) * .25f
			};

			// ITU-R BT.601 standard RGB-to-YPbPr
			const float f[3][4] =
			{
				{  .299f,  .587f,  .114f,   0.f  },
				{ -.169f, -.331f,  .499f,   .5f  },
				{  .499f, -.418f, -.0813f,  .5f  }
			};

			const float y[4] =
			{
				f[0][0] * rgb[0][0] + f[0][1] * rgb[0][1] + f[0][2] * rgb[0][2],
				f[0][0] * rgb[1][0] + f[0][1] * rgb[1][1] + f[0][2] * rgb[1][2],
				f[0][0] * rgb[2][0] + f[0][1] * rgb[2][1] + f[0][2] * rgb[2][2],
				f[0][0] * rgb[3][0] + f[0][1] * rgb[3][1] + f[0][2] * rgb[3][2]
			};

			const float u =
				f[1][0] * rgb_filt[0] + f[1][1] * rgb_filt[1] + f[1][2] * rgb_filt[2] + f[1][3];

			const float v =
				f[2][0] * rgb_filt[0] + f[2][1] * rgb_filt[1] + f[2][2] * rgb_filt[2] + f[2][3];

			y_dst[0][0] = uint8_t(y[0] * 255.f);
			y_dst[0][1] = uint8_t(y[1] * 255.f);
			y_dst[1][0] = uint8_t(y[2] * 255.f);
			y_dst[1][1] = uint8_t(y[3] * 255.f);

			u_dst[0] = uint8_t(u * 255.f);
			v_dst[0] = uint8_t(v * 255.f);

			rgb_src[0] += 2;
			rgb_src[1] += 2;

			y_dst[0] += 2;
			y_dst[1] += 2;
			u_dst += 1;
			v_dst += 1;
		}

		rgb_src[0] = (pix*) ((uint8_t*) rgb_src[1] + rgb_pad);
		rgb_src[1] = (pix*) ((uint8_t*) rgb_src[0] + rgb_stride);

		y_dst[0] = y_dst[1] + y_pad;
		y_dst[1] = y_dst[0] + y_stride;

		u_dst += u_pad;
		v_dst += v_pad;
	}
}


bool
testbed::util::setupTexture2D(
	const GLuint tex_name,
	const char* filename,
	const unsigned tex_w,
	const unsigned tex_h)
{
	const unsigned pix_size = sizeof(pix);
	const unsigned tex_size = tex_h * tex_w * pix_size;

	// provide some guardband as pixels are of non-word-multiple size
	pix* const tex_src = (pix *) malloc(next_multiple_of_pix_integral(tex_size));
	assert(tex_src);

	if (fill_from_file(tex_src, tex_w * pix_size, tex_w, tex_h, filename))
	{
		std::cout << "texture bitmap '" << filename << "' ";
	}
	else
	{
		fill_with_checker(tex_src, tex_w * pix_size, tex_w, tex_h);

		std::cout << "checker texture ";
	}

	std::cout << tex_w << " x " << tex_h << " x " << (pix_size * 8) << " bpp, " << tex_size << " bytes" << std::endl;

	const bool pot = !(tex_w & tex_w - 1) && !(tex_h & tex_h - 1);

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, tex_name);

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, pot ? GL_LINEAR_MIPMAP_NEAREST : GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, tex_w, tex_h, 0, GL_RGB, GL_UNSIGNED_BYTE, 0);
	glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, tex_w, tex_h, GL_RGB, GL_UNSIGNED_BYTE, tex_src);

	if (pot)
	{
		std::cout << "expanded into a mipmap" << std::endl;

		glGenerateMipmap(GL_TEXTURE_2D);
	}

	glBindTexture(GL_TEXTURE_2D, 0);

	free(tex_src);

	const bool success = !reportGLError();

	if (!success)
		std::cerr << __FUNCTION__ << " failed to setup a texture" << std::endl;

	return success;
}


bool
testbed::util::setupTextureYUV420(
	const GLuint (&tex_name)[3],
	const char* filename,
	const unsigned tex_w,
	const unsigned tex_h)
{
	const unsigned pix_size = sizeof(pix);
	const unsigned tex_size = tex_h * tex_w * pix_size;

	// provide some guardband as pixels are of non-word-multiple size
	pix* const tex_src = (pix *) malloc(next_multiple_of_pix_integral(tex_size));
	assert(tex_src);

	if (fill_from_file(tex_src, tex_w * pix_size, tex_w, tex_h, filename))
	{
		std::cout << "texture bitmap '" << filename << "' ";
	}
	else
	{
		fill_with_checker(tex_src, tex_w * pix_size, tex_w, tex_h);

		std::cout << "checker texture ";
	}

	std::cout << tex_w << " x " << tex_h << " x " << (pix_size * 8) << " bpp, " << tex_size << " bytes" << std::endl;

	uint8_t* const yuv_tex_src = (uint8_t *) malloc(tex_w * tex_h * 3 / 2);

	uint8_t* y_tex_src = yuv_tex_src;
	uint8_t* u_tex_src = yuv_tex_src + tex_w * tex_h;
	uint8_t* v_tex_src = yuv_tex_src + tex_w * tex_h * 5 / 4;

	fill_YUV420_from_RGB(
		y_tex_src, u_tex_src, v_tex_src,
		tex_w, tex_w / 2, tex_w / 2,
		tex_src,
		tex_w * pix_size,
		tex_w, tex_h);

	free(tex_src);

	const bool pot = !(tex_w & tex_w - 1) && !(tex_h & tex_h - 1);

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, tex_name[0]);

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, pot ? GL_LINEAR_MIPMAP_NEAREST : GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

	glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, tex_w, tex_h, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, 0);
	glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, tex_w, tex_h, GL_LUMINANCE, GL_UNSIGNED_BYTE, y_tex_src);

	if (pot)
	{
		std::cout << "expanded into a mipmap" << std::endl;

		glGenerateMipmap(GL_TEXTURE_2D);
	}

	glBindTexture(GL_TEXTURE_2D, tex_name[1]);

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, pot ? GL_LINEAR_MIPMAP_NEAREST : GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

	glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, tex_w / 2, tex_h / 2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, 0);
	glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, tex_w / 2, tex_h / 2, GL_LUMINANCE, GL_UNSIGNED_BYTE, u_tex_src);

	if (pot)
	{
		std::cout << "expanded into a mipmap" << std::endl;

		glGenerateMipmap(GL_TEXTURE_2D);
	}

	glBindTexture(GL_TEXTURE_2D, tex_name[2]);

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, pot ? GL_LINEAR_MIPMAP_NEAREST : GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

	glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, tex_w / 2, tex_h / 2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, 0);
	glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, tex_w / 2, tex_h / 2, GL_LUMINANCE, GL_UNSIGNED_BYTE, v_tex_src);

	if (pot)
	{
		std::cout << "expanded into a mipmap" << std::endl;

		glGenerateMipmap(GL_TEXTURE_2D);
	}

	glBindTexture(GL_TEXTURE_2D, 0);

	free(yuv_tex_src);

	const bool success = !reportGLError();

	if (!success)
		std::cerr << __FUNCTION__ << " failed to setup a texture" << std::endl;

	return success;
}


bool
testbed::util::setupTexture2D(
	const GLuint tex_name,
	const pix* tex_src,
	const unsigned tex_w,
	const unsigned tex_h)
{
	assert(tex_src);

	const unsigned pix_size = sizeof(pix);
	const unsigned tex_size = tex_h * tex_w * pix_size;

	std::cout << "client texture " << tex_src << " " <<
		tex_w << " x " << tex_h << " x " << (pix_size * 8) << " bpp, " << tex_size << " bytes" << std::endl;

	const bool pot = !(tex_w & tex_w - 1) && !(tex_h & tex_h - 1);

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, tex_name);

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, pot ? GL_NEAREST_MIPMAP_NEAREST : GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, tex_w, tex_h, 0, GL_RGB, GL_UNSIGNED_BYTE, 0);
	glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, tex_w, tex_h, GL_RGB, GL_UNSIGNED_BYTE, tex_src);

	if (pot)
	{
		std::cout << "expanded into a mipmap" << std::endl;

		glGenerateMipmap(GL_TEXTURE_2D);
	}

	glBindTexture(GL_TEXTURE_2D, 0);

	const bool success = !reportGLError();

	if (!success)
		std::cerr << __FUNCTION__ << " failed to setup a texture" << std::endl;

	return success;
}


bool
testbed::util::setupShader(
	const GLuint shader_name,
	const char* source_filename)
{
	FILE* source_file = fopen(source_filename, "r");

	if (!source_file)
	{
		std::cerr << "bad shader source '" << source_filename << "'" << std::endl;
		return false;
	}

	GLint success = GL_FALSE;

	const unsigned source_max_len = 4096;
	static char source[source_max_len];
	const char* src = source;

	const GLint read = fread(source, sizeof(source[0]), source_max_len, source_file);

	if (read && (read != source_max_len || feof(source_file)))
	{
		glShaderSource(shader_name, 1, &src, &read);
		glCompileShader(shader_name);

		const GLsizei log_max_len = 1024;
		static char log[log_max_len + 1];
		GLsizei log_len;

		glGetShaderInfoLog(shader_name, log_max_len, &log_len, log);

		log[log_len] = '\0';
		std::cerr << "shader compile log: " << log << std::endl;

		glGetShaderiv(shader_name, GL_COMPILE_STATUS, &success);
	}
	else
		std::cerr << "bad shader source length for '" << source_filename << "'" << std::endl;

	fclose(source_file);

	return GL_FALSE != success;
}


bool
testbed::util::setupProgram(
	const GLuint prog,
	const GLuint shader_v,
	const GLuint shader_f)
{
	if (!prog || !shader_v || !shader_f)
		return false;

	glAttachShader(prog, shader_v);
	glAttachShader(prog, shader_f);

	glLinkProgram(prog);

	const GLsizei log_max_len = 1024;
	static char log[log_max_len + 1];
	GLsizei log_len;

	glGetProgramInfoLog(prog, log_max_len, &log_len, log);

	log[log_len] = '\0';
	std::cerr << "shader link log: " << log << std::endl;

	GLint success;
	glGetProgramiv(prog, GL_LINK_STATUS, &success);

	return GL_FALSE != success;
}


bool
testbed::util::fill_indexed_mesh_from_file_PN(
	const char* filename,
	const GLuint vbo_arr,
	const GLuint vbo_idx,
	unsigned& num_faces,
	const bool is_rotated)
{
	typedef uint16_t Index;

	assert(filename);

	struct scoped_t
	{
		FILE* file;
		void* vbuf;
		void* ibuf;

		scoped_t()
		: file(0)
		, vbuf(0)
		, ibuf(0)
		{}

		~scoped_t()
		{
			if (0 != file)
				fclose(file);
			free(vbuf);
			free(ibuf);
		}
	}
	scoped;

	if (0 == (scoped.file = fopen(filename, "rb")))
	{
		std::cerr << __FUNCTION__ << " failed at fopen '" << filename << "'" << std::endl;
		return false;
	}

	unsigned nv = 0;

	if (1 != fscanf(scoped.file, "%u", &nv))
		return false;

	if (nv > 1 << sizeof(Index) * 8)
		return false;

	float vmin[3] = {  FLT_MAX,  FLT_MAX,  FLT_MAX };
	float vmax[3] = { -FLT_MAX, -FLT_MAX, -FLT_MAX };

	enum { NUM_FLOATS = 6 };
	const unsigned sizeof_vbuf = sizeof(float) * NUM_FLOATS * nv;
	scoped.vbuf = malloc(sizeof_vbuf);

	if (0 == scoped.vbuf)
		return false;

	for (unsigned i = 0; i < nv; ++i)
	{
		float (&vi)[NUM_FLOATS] = reinterpret_cast< float (*)[NUM_FLOATS] >(scoped.vbuf)[i];

		if (NUM_FLOATS != fscanf(scoped.file, "%f %f %f %f %f %f",
					&vi[0], &vi[1], &vi[2],
					&vi[3], &vi[4], &vi[5]))
		{
			return false;
		}

		if (vmin[0] > vi[0])
			vmin[0] = vi[0];
		if (vmin[1] > vi[1])
			vmin[1] = vi[1];
		if (vmin[2] > vi[2])
			vmin[2] = vi[2];

		if (vmax[0] < vi[0])
			vmax[0] = vi[0];
		if (vmax[1] < vi[1])
			vmax[1] = vi[1];
		if (vmax[2] < vi[2])
			vmax[2] = vi[2];
	}

	const float span = (vmax[0] - vmin[0]) > (vmax[1] - vmin[1])
		? ((vmax[0] - vmin[0]) > (vmax[2] - vmin[2]) ? vmax[0] - vmin[0] : vmax[2] - vmin[2])
		: ((vmax[1] - vmin[1]) > (vmax[2] - vmin[2]) ? vmax[1] - vmin[1] : vmax[2] - vmin[2]);

	const float origin[3] = {
		(vmin[0] + vmax[0]) * .5f,
		(vmin[1] + vmax[1]) * .5f,
		(vmin[2] + vmax[2]) * .5f
	};

	for (unsigned i = 0; i < nv; ++i)
	{
		float (&vi)[NUM_FLOATS] = reinterpret_cast< float (*)[NUM_FLOATS] >(scoped.vbuf)[i];

		vi[0] -= origin[0];
		vi[1] -= origin[1];
		vi[2] -= origin[2];

		vi[0] /= span * .5f;
		vi[1] /= span * .5f;
		vi[2] /= span * .5f;

		if (is_rotated)
		{
			const float vi_1 = -vi[1];
			vi[1] = vi[2];
			vi[2] = vi_1;

			const float vi_4 = -vi[4];
			vi[4] = vi[5];
			vi[5] = vi_4;
		}
	}

	unsigned nt = 0;

	if (1 != fscanf(scoped.file, "%u", &nt))
		return false;

	const unsigned sizeof_ibuf = sizeof(Index) * 3 * nt;
	scoped.ibuf = malloc(sizeof_ibuf);

	if (0 == scoped.ibuf)
		return false;

	for (unsigned i = 0; i < nt; ++i)
	{
		Index (&ti)[3] = reinterpret_cast< Index (*)[3] >(scoped.ibuf)[i];

		if (3 != fscanf(scoped.file, "%hu %hu %hu",
					&ti[0],
					&ti[1],
					&ti[2]))
		{
			return false;
		}
	}

	std::cout << "number of vertices: " << nv << "\nnumber of indices: " << nt * 3 << std::endl;

	glBindBuffer(GL_ARRAY_BUFFER, vbo_arr);
	glBufferData(GL_ARRAY_BUFFER, sizeof_vbuf, scoped.vbuf, GL_STATIC_DRAW);

	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo_idx);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof_ibuf, scoped.ibuf, GL_STATIC_DRAW);

	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

	num_faces = nt;

	return true;
}


bool
testbed::util::fill_indexed_mesh_from_file_PN2(
	const char* filename,
	const GLuint vbo_arr,
	const GLuint vbo_idx,
	unsigned& num_faces,
	const bool is_rotated)
{
	typedef uint16_t Index;

	assert(filename);

	struct scoped_t
	{
		FILE* file;
		void* vbuf;
		void* ibuf;

		scoped_t()
		: file(0)
		, vbuf(0)
		, ibuf(0)
		{}

		~scoped_t()
		{
			if (0 != file)
				fclose(file);
			free(vbuf);
			free(ibuf);
		}
	}
	scoped;

	if (0 == (scoped.file = fopen(filename, "rb")))
	{
		std::cerr << __FUNCTION__ << " failed at fopen '" << filename << "'" << std::endl;
		return false;
	}

	unsigned nv = 0;

	if (1 != fscanf(scoped.file, "%u", &nv))
		return false;

	if (nv > 1 << sizeof(Index) * 8)
		return false;

	float vmin[3] = {  FLT_MAX,  FLT_MAX,  FLT_MAX };
	float vmax[3] = { -FLT_MAX, -FLT_MAX, -FLT_MAX };

	enum { NUM_FLOATS = 8 };
	const unsigned sizeof_vbuf = sizeof(float) * NUM_FLOATS * nv;
	scoped.vbuf = malloc(sizeof_vbuf);

	if (0 == scoped.vbuf)
		return false;

	for (unsigned i = 0; i < nv; ++i)
	{
		float (&vi)[NUM_FLOATS] = reinterpret_cast< float (*)[NUM_FLOATS] >(scoped.vbuf)[i];

		if (NUM_FLOATS != fscanf(scoped.file, "%f %f %f %f %f %f %f %f",
					&vi[0], &vi[1], &vi[2],
					&vi[3], &vi[4], &vi[5],
					&vi[6], &vi[7]))
		{
			return false;
		}

		if (vmin[0] > vi[0])
			vmin[0] = vi[0];
		if (vmin[1] > vi[1])
			vmin[1] = vi[1];
		if (vmin[2] > vi[2])
			vmin[2] = vi[2];

		if (vmax[0] < vi[0])
			vmax[0] = vi[0];
		if (vmax[1] < vi[1])
			vmax[1] = vi[1];
		if (vmax[2] < vi[2])
			vmax[2] = vi[2];
	}

	const float span = (vmax[0] - vmin[0]) > (vmax[1] - vmin[1])
		? ((vmax[0] - vmin[0]) > (vmax[2] - vmin[2]) ? vmax[0] - vmin[0] : vmax[2] - vmin[2])
		: ((vmax[1] - vmin[1]) > (vmax[2] - vmin[2]) ? vmax[1] - vmin[1] : vmax[2] - vmin[2]);

	const float origin[3] = {
		(vmin[0] + vmax[0]) * .5f,
		(vmin[1] + vmax[1]) * .5f,
		(vmin[2] + vmax[2]) * .5f
	};

	for (unsigned i = 0; i < nv; ++i)
	{
		float (&vi)[NUM_FLOATS] = reinterpret_cast< float (*)[NUM_FLOATS] >(scoped.vbuf)[i];

		vi[0] -= origin[0];
		vi[1] -= origin[1];
		vi[2] -= origin[2];

		vi[0] /= span * .5f;
		vi[1] /= span * .5f;
		vi[2] /= span * .5f;

		if (is_rotated)
		{
			const float vi_1 = -vi[1];
			vi[1] = vi[2];
			vi[2] = vi_1;

			const float vi_4 = -vi[4];
			vi[4] = vi[5];
			vi[5] = vi_4;
		}
	}

	unsigned nt = 0;

	if (1 != fscanf(scoped.file, "%u", &nt))
		return false;

	const unsigned sizeof_ibuf = sizeof(Index) * 3 * nt;
	scoped.ibuf = malloc(sizeof_ibuf);

	if (0 == scoped.ibuf)
		return false;

	for (unsigned i = 0; i < nt; ++i)
	{
		Index (&ti)[3] = reinterpret_cast< Index (*)[3] >(scoped.ibuf)[i];

		if (3 != fscanf(scoped.file, "%hu %hu %hu",
					&ti[0],
					&ti[1],
					&ti[2]))
		{
			return false;
		}
	}

	std::cout << "number of vertices: " << nv << "\nnumber of indices: " << nt * 3 << std::endl;

	glBindBuffer(GL_ARRAY_BUFFER, vbo_arr);
	glBufferData(GL_ARRAY_BUFFER, sizeof_vbuf, scoped.vbuf, GL_STATIC_DRAW);

	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo_idx);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof_ibuf, scoped.ibuf, GL_STATIC_DRAW);

	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

	num_faces = nt;

	return true;
}


static void
saveFramebuffer(
	const unsigned window_w,
	const unsigned window_h,
	const char* name = NULL)
{
	const unsigned pixels_len = 4 * window_h * window_w;
	GLvoid * const pixels = malloc(pixels_len);

	glReadPixels(0, 0, window_w, window_h, GL_RGBA, GL_UNSIGNED_BYTE, pixels);

	const time_t t = time(NULL);
	const tm* tt = localtime(&t);

	char default_name[FILENAME_MAX + 1];

	if (!name || !name[0])
	{
		const char nameBase[] = "frame";
		const char nameExt[] = ".raw";

		sprintf(default_name, "%s%04d%03d%02d%02d%02d%s",
			nameBase, tt->tm_year, tt->tm_yday, tt->tm_hour, tt->tm_min, tt->tm_sec, nameExt);

		name = default_name;
	}

	std::cout << "saving framegrab as '" << name << "'" << std::endl;

	FILE* f = fopen(name, "wb");

	if (f)
	{
		const unsigned dims[] = { window_h, window_w };

		fwrite(dims, sizeof(dims), 1, f);
		fwrite(pixels, pixels_len, 1, f);

		fclose(f);
	}

	free(pixels);
}


static bool
validate_fullscreen(
	const char* string,
	unsigned &screen_w,
	unsigned &screen_h)
{
	if (!string)
		return false;

	unsigned x, y, hz;

	if (3 != sscanf(string, "%u %u %u", &x, &y, &hz))
		return false;

	if (!x || !y || !hz)
		return false;

	screen_w = x;
	screen_h = y;

	return true;
}


static bool
validate_bitness(
	const char* string,
	unsigned (&screen_bitness)[4])
{
	if (!string)
		return false;

	unsigned bitness[4];

	if (4 != sscanf(string, "%u %u %u %u", &bitness[0], &bitness[1], &bitness[2], &bitness[3]))
		return false;

	if (!bitness[0] || 16 < bitness[0] ||
		!bitness[1] || 16 < bitness[1] ||
		!bitness[2] || 16 < bitness[2] ||
		16 < bitness[3])
	{
		return false;
	}

	screen_bitness[0] = bitness[0];
	screen_bitness[1] = bitness[1];
	screen_bitness[2] = bitness[2];
	screen_bitness[3] = bitness[3];

	return true;
}

static GLuint g_input;

static bool
processEvents(
	Display* display,
	const Window& window)
{
	XEvent event;
	char buffer[16];

	while (XPending(display) > 0)
    {
		XNextEvent(display, &event);

		switch (event.type)
		{
		case ClientMessage:
			if (event.xclient.message_type == wm_protocols &&
				event.xclient.data.l[0] == wm_delete_window)
			{
				return false;
			}
			break;

		case KeyPress:
		case KeyRelease:

			XLookupString((XKeyPressedEvent *) &event, buffer, sizeof(buffer) - 1, NULL, NULL);

			switch (buffer[0])
			{
			case 27:
				if (KeyPress == event.type)
					g_input |= testbed::hook::INPUT_MASK_ESC;
				else
					g_input &= ~testbed::hook::INPUT_MASK_ESC;
				break;
			case 'j':
			case 'J':
				if (KeyPress == event.type)
					g_input |= testbed::hook::INPUT_MASK_LEFT;
				else
					g_input &= ~testbed::hook::INPUT_MASK_LEFT;
				break;
			case 'm':
			case 'M':
				if (KeyPress == event.type)
					g_input |= testbed::hook::INPUT_MASK_DOWN;
				else
					g_input &= ~testbed::hook::INPUT_MASK_DOWN;
				break;
			case 'l':
			case 'L':
				if (KeyPress == event.type)
					g_input |= testbed::hook::INPUT_MASK_RIGHT;
				else
					g_input &= ~testbed::hook::INPUT_MASK_RIGHT;
				break;
			case 'i':
			case 'I':
				if (KeyPress == event.type)
					g_input |= testbed::hook::INPUT_MASK_UP;
				else
					g_input &= ~testbed::hook::INPUT_MASK_UP;
				break;
			}
			break;

		default:
			// drop this event
			break;
		}
    }

	return true;
}


static inline unsigned
bitcount(
	unsigned n)
{
	unsigned count = 0;

	while (n)
	{
		count++;
		n &= (n - 1);
	}

	return count;
}


int main(
	int argc,
	char** argv)
{
	unsigned fsaa = 0;
	unsigned frames = -1U;
	unsigned grab_frame = -1U;
	char grab_filename[FILENAME_MAX + 1] = { 0 };
	unsigned w = 512, h = 512;
	unsigned bitness[4] = { 0 };
	unsigned config_id = 0;

	bool cli_err = false;
	bool print_configs = false;
	bool print_perf_counters = false;

	const unsigned prefix_len = strlen(testbed::arg_prefix);

	#define QUOTE(x)	#x
	#define XQUOTE(x)	QUOTE(x)

	for (int i = 1; i < argc && !cli_err; ++i)
	{
		if (strncmp(argv[i], testbed::arg_prefix, prefix_len))
		{
			cli_err = true;
			continue;
		}

		if (!strcmp(argv[i] + prefix_len, testbed::arg_app))
		{
			if (++i == argc)
				cli_err = true;

			continue;
		}

		if (!strcmp(argv[i] + prefix_len, arg_nframes))
		{
			if (!(++i < argc) || (1 != sscanf(argv[i], "%u", &frames)))
				cli_err = true;

			continue;
		}

		if (!strcmp(argv[i] + prefix_len, arg_screen))
		{
			if (!(++i < argc) || !validate_fullscreen(argv[i], w, h))
				cli_err = true;

			continue;
		}

		if (!strcmp(argv[i] + prefix_len, arg_bitness))
		{
			if (!(++i < argc) || !validate_bitness(argv[i], bitness))
				cli_err = true;

			continue;
		}

		if (!strcmp(argv[i] + prefix_len, arg_config_id))
		{
			if (!(++i < argc) || (1 != sscanf(argv[i], "%u", &config_id)) || !config_id)
				cli_err = true;

			continue;
		}

		if (!strcmp(argv[i] + prefix_len, arg_fsaa))
		{
			if (!(++i < argc) || (1 != sscanf(argv[i], "%u", &fsaa)))
				cli_err = true;

			continue;
		}

		if (!strcmp(argv[i] + prefix_len, arg_grab))
		{
			if (!(++i < argc) || !sscanf(argv[i], "%u %" XQUOTE(FILENAME_MAX) "s", &grab_frame, grab_filename))
				cli_err = true;

			continue;
		}

		if (!strcmp(argv[i] + prefix_len, arg_print_configs))
		{
			print_configs = true;
			continue;
		}

		if (!strcmp(argv[i] + prefix_len, arg_print_perf))
		{
			print_perf_counters = true;
			continue;
		}

		cli_err = true;
	}

	#undef XQUOTE
	#undef QUOTE

	if (cli_err)
	{
		std::cerr << "usage: " << argv[0] << " [<option> ...]\n"
			"options (multiple args to an option must constitute a single string, eg. -foo \"a b c\"):\n"
			"\t" << testbed::arg_prefix << arg_nframes <<
			" <unsigned_integer>\t\t: set number of frames to run; default is max unsigned int\n"
			"\t" << testbed::arg_prefix << arg_screen <<
			" <width> <height> <Hz>\t\t: set fullscreen output of specified geometry and refresh\n"
			"\t" << testbed::arg_prefix << arg_bitness <<
			" <r> <g> <b> <a>\t\t: set EGL config of specified RGBA bitness; default is screen's bitness\n"
			"\t" << testbed::arg_prefix << arg_config_id <<
			" <positive_integer>\t\t: set EGL config of specified ID; overrides any other config options\n"
			"\t" << testbed::arg_prefix << arg_fsaa <<
			" <positive_integer>\t\t: set fullscreen antialiasing; default is none\n"
			"\t" << testbed::arg_prefix << arg_grab <<
			" <unsigned_integer> [<file>]\t: grab the Nth frame to file; index is zero-based\n"
			"\t" << testbed::arg_prefix << arg_print_configs <<
			"\t\t\t: print available EGL configs\n"
			"\t" << testbed::arg_prefix << arg_print_perf <<
			"\t\t\t: print available GPU performance monitor groups and counters\n"
			"\t" << testbed::arg_prefix << testbed::arg_app <<
			" <option> [<arguments>]\t\t: app-specific option" << std::endl;

		return 1;
	}

	Display *display = XOpenDisplay(NULL);

	if (!display)
	{
		std::cerr << "failed to open display" << std::endl;
		return 1;
	}

	if (!config_id && !bitness[0])
	{
		const int screen = XDefaultScreen(display);

		XVisualInfo vinfo_template;
		memset(&vinfo_template, 0, sizeof(vinfo_template));

		vinfo_template.visualid = XVisualIDFromVisual(XDefaultVisual(display, screen));
		vinfo_template.screen = screen;

		const long vinfo_mask = VisualIDMask | VisualScreenMask;

		int n_vinfo = 0;
		XVisualInfo* vinfo = XGetVisualInfo(display, vinfo_mask, &vinfo_template, &n_vinfo);

		bitness[0] = bitcount(vinfo->red_mask);
		bitness[1] = bitcount(vinfo->green_mask);
		bitness[2] = bitcount(vinfo->blue_mask);
		bitness[3] = 0;

		XFree(vinfo);

		std::cout << "Using screen RGB bitness: " <<
			bitness[0] << " " <<
			bitness[1] << " " <<
			bitness[2] << std::endl;
	}

	const Window root = XDefaultRootWindow(display);

#ifdef USE_XRANDR

	int dummy;

	if (!XQueryExtension(display, "RANDR", &dummy, &dummy, &dummy))
	{
		std::cerr << "XRANDR not found" << std::endl;
		return 1;
	}

	XRRScreenConfiguration *config = 0;
	XRRScreenSize *sizes = 0;
	Rotation current_rotation;
	int nsizes = 0;

	config = XRRGetScreenInfo(display, root);
	sizes = XRRConfigSizes(config, &nsizes);

	if (config && nsizes > 0)
	{
		XRRConfigCurrentConfiguration(config, &current_rotation);

		std::cout << "XRR screen config 0: " << sizes[0].width << " x " << sizes[0].height << std::endl;
		int mode = 0;

		for (int i = 1; i < nsizes; ++i)
		{
			std::cout << "XRR screen config " << i << ": " << sizes[i].width << " x " << sizes[i].height << std::endl;

			if (sizes[i].width >= (int) w &&
				sizes[i].height >= (int) h &&
				(sizes[mode].width < (int) w || sizes[mode].width > sizes[i].width ||
				 sizes[mode].height < (int) h || sizes[mode].height > sizes[i].height))
			{
				mode = i;
			}
		}

		if (sizes[mode].width < (int) w || sizes[mode].height < (int) h)
		{
			std::cerr << "failed to find a suitable screen config through XRANDR" << std::endl;
			return 1;
		}

		XRRSetScreenConfig(display, config, root, mode, current_rotation, CurrentTime);
		XRRFreeScreenConfigInfo(config);
	}
	else
	{
		std::cerr << "failed to obtain screen configs through XRANDR" << std::endl;
		return 1;
	}

#endif // USE_XRANDR

	const unsigned border_width = 0;

	const int x = 0, y = 0;
	const unsigned long border = 0, background = 0;

	const Window window = XCreateSimpleWindow(display, root, x, y, w, h, border_width, border, background);

	if (!window)
	{
		XCloseDisplay(display);

		std::cerr << "failed to create a window" << std::endl;
		return 1;
	}

	wm_protocols = XInternAtom(display, "WM_PROTOCOLS", False);
	wm_delete_window = XInternAtom(display, "WM_DELETE_WINDOW", False);

	XSetWMProtocols(display, window, &wm_delete_window, 1);

	XMapWindow(display, window);
	XFlush(display);

	XSelectInput(display, window, KeyPressMask | KeyReleaseMask);

	int exit_code = 0;
	EGL egl;

	if (egl.initGLES2(display, window, config_id, fsaa, bitness[0], bitness[1], bitness[2], bitness[3], print_configs) &&
		reportGLCaps() &&
		testbed::hook::init_resources(argc, argv))
	{
		amd_perf_monitor::load_extension();
		amd_perf_monitor::peek_performance_monitor(print_perf_counters);

		unsigned nframes = 0;
		const uint64_t t0 = timer_nsec();
		uint64_t tq = 0;

		while (processEvents(display, window) && nframes < frames)
		{
			if (!testbed::hook::render_frame())
				break;

			if (nframes == grab_frame)
				saveFramebuffer(w, h, grab_filename);

			egl.swapBuffers();

			uint64_t tf = timer_nsec();

			const uint64_t dt = tf - tq;
			const uint64_t tt = 1000000000ULL / 60;

			tq = tf;

			if (!testbed::hook::input_frame(g_input))
				break;

			++nframes;

			if (tt > dt)
			{
				timespec req = { 0, tt - dt };
				timespec rem;

				while (nanosleep(&req, &rem) && EINTR == errno)
					req = rem;
#if 0
				tq = timer_nsec();
#else
				tq += tt - dt;
#endif
			}
		}

		const uint64_t dt = timer_nsec() - t0;

		std::cout << "total frames rendered: " << nframes << std::endl;

		if (dt)
		{
			const double sec = double(dt) * 1e-9;

			std::cout << "elapsed time: " << sec << " s"
				"\naverage FPS: " << (double(nframes) / sec) << std::endl;
		}

		amd_perf_monitor::depeek_performance_monitor();

		testbed::hook::deinit_resources();
	}
	else
	{
		std::cerr << "failed to initialise either GLES or resources; bailing out" << std::endl;
		exit_code = 1;
	}

	egl.deinit();

	XDestroyWindow(display, window);
	XCloseDisplay(display);

	return exit_code;
}
